Skip to content

feat(driver-sql,driver-turso): the remaining IDataDriver doors publish their declared types, not any (#15267) - #17258

Merged
os-sam merged 6 commits into
mainfrom
claude/issue-15267-sql-driver-declared-doors
Sep 10, 2026
Merged

feat(driver-sql,driver-turso): the remaining IDataDriver doors publish their declared types, not any (#15267)#17258
os-sam merged 6 commits into
mainfrom
claude/issue-15267-sql-driver-declared-doors

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #15267

Generic type arguments are written in SQUARE brackets throughout this body (the body sanitizer eats the angle-bracket spelling): Promise[any] in prose means the angle-bracket form in code.

What

SqlDriver published an explicit Promise[any] over five IDataDriver doors that packages/spec/src/contracts/data-driver.ts had already declared narrower. An explicit any satisfies every one of them structurally, so tsc said nothing while the emitted .d.ts told every consumer that findOne() never returns null and that create() returns whatever they like. #15280 (card #14438) un-masked update() and filed the census of what was left; this is that remainder.

Cited by symbol, as the contract declares them:

door IDataDriver symbol contract declares was
findOne IDataDriver.findOne Promise[Record[string, unknown] | null] Promise[any]
create IDataDriver.create Promise[Record[string, unknown]] Promise[any]
bulkCreate IDataDriver.bulkCreate Promise[Record[string, unknown][]] Promise[any]
execute IDataDriver.execute Promise[unknown] Promise[any]
explain IDataDriver.explain (optional) Promise[unknown] Promise[any]

Every one was already declared narrower on the contract before this PR — verified by reading the file, not the card. So no door here is the first place its narrower type is published, and ruling 2's stop-and-report condition did not fire.

No runtime behaviour changes. Each annotation is now the contract's, and each is pinned at the type level inside its own package's tsc program.

Scope correction: driver-turso overrides FOUR of the five doors, not one

The card and the dispatch both record that TursoDriver overrides create() and that this override will not inherit a driver-sql fix. That is true but incomplete. Measured on origin/main:

turso-driver.ts override was now
findOne Promise[any] Promise[Record[string, unknown] | null]
create Promise[any] Promise[Record[string, unknown]]
bulkCreate Promise[any] Promise[Record[string, unknown][]]
execute Promise[any] Promise[unknown]

Fixing only create would have left @objectstack/driver-turso's published .d.ts declaring findOne, bulkCreate and execute as any on its own — the census would read clean while three doors stayed masked. That is precisely the recurrence the triage seat flagged twice, one door wider than it was measured. These are additional SITES of the same five doors, not additional doors: analyzeQuery, aggregate, upsert and beginTransaction are untouched.

The fix is mechanical and provably safe, because both branches of all four already answered the contract's type before this change: the local branch forwards to super.[door] (narrowed here), and the remote branch passes RemoteTransport's result — already declared Record[string, unknown] | null, Record[string, unknown], Record[string, unknown][] and unknown respectively — through the generic formatRemoteRow / formatRemoteRows ([T](object, row: T): T). Each override's Promise[any] was pure erasure with nothing behind it.

Semver grade: minor + BREAKING

The dispatch's ruling 2 said patch, on the reading that the contract already declares these returns so the classes merely stop publishing wider than their own declaration. The seat corrected that grade after measuring the public-entry exports, and the correction is right: SqlDriver and TursoDriver are themselves exported from their packages' public entries (packages/drivers/driver-sql/src/index.ts, packages/drivers/driver-turso/src/index.ts), so a consumer can import { SqlDriver } and type against the class directly. For that consumer the class's own .d.ts IS the contract, and narrowing findOne from Promise[any] to Promise[Record[string, unknown] | null] breaks their build — they now owe a null check they did not owe before. That is the published-type-narrowing rung: minor + BREAKING + an adr-0087: disposition.

The landed precedent independently settles it the same way. Ruling 4 asked what #14438 actually did. Measured from PR #15280's merged diff, both of its changesets are minor, not patch:

  • .changeset/driver-sql-update-declared-null.md'@objectstack/driver-sql': minor
  • .changeset/driver-turso-update-declared-null.md'@objectstack/driver-turso': minor

both bodies opening **BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as minor under the launch-window convention (the one PR #14434 used), and both carrying a type-surface-only ADR-0087 marker. So the precedent and the corrected ruling agree; the original patch reading was the outlier, and nothing here had to choose between them.

Both changesets are graded minor, declare BREAKING, and carry a type-surface-only ADR-0087 disposition. No Clause-②, no needs:contract-review — matching both ruling 2's standing instruction and the precedent PR's own label set. packages/spec/ is untouched.

One boundary worth a reviewer's eye

check-adr-0087-registration.mjs verifies a type-surface-only claim with predicate 4, "narrowed-from-erased", and its isErasedType counts both any and unknown as erased — pinned deliberately in its self-test (TSO-U5 / TSO-U6). So execute and explain, whose destination is the contract's own unknown, cannot serve as predicate-4 evidence even though moving them off any genuinely breaks TypeScript consumers (an unknown refuses the property reads any allowed — this PR had to repair exactly such a read). The markers therefore name the three doors that move onto concrete shapes and state the remaining doors in prose; the disposition is identical for all of them. Reported as a finding, not routed around.

What the narrowing surfaced — the point of the change

69 un-narrowed dereferences that the any had been hiding, every one in test code, none in production code:

package sites what they were
driver-sql 68 64 reads of a findOne() result with no null check, plus 4 reads through the new unknown
driver-turso 1 expect(row).not.toBeNull() followed by a dereference
driver-sqlite-wasm 10 inherits every door; same two shapes
runtime 1 create().rec_no in the autonumber cross-side parity probe

The dominant shape is a positive control that wrote expect(x).not.toBeNull() and then dereferenced — that assertion does not narrow. Each is repaired with vitest's assert(), typed as an assertion signature, so it narrows: not a ! and not a cast, the spelling PR #15280 established. The not-found controls keep their toBeNull() and gained nothing. Reads through the new unknown name what they read (String(...), or a named plan slice for explain()). No test was skipped, disabled, quarantined or marked .todo.

@objectstack/driver-sqlite-wasm overrides none of the five doors and re-declares no member of its own, so it carries no changeset — the narrowing reaches its consumers through @objectstack/driver-sql's .d.ts. Its test-side repairs are here because the DoD requires fixing callers the narrowing legitimately breaks.

Region fence

PR #17221 is open against sql-driver.ts with hunks at @@ -9473, @@ -9589, @@ -9641; the declared fence is lines 9460–9690. Every hunk in this PR, in merge-base coordinates: 6053, 6055, 6067, 7888, 8431, 8433, 9240. The highest is 220 lines clear of the fence. No open PR touches turso-driver.ts.

Verification

Consumer-closure typecheck — the prefix (downstream) direction, every workspace package that depends on either edited package, with their builds:

pnpm exec turbo run typecheck --filter='...@objectstack/driver-sql' --filter='...@objectstack/driver-turso'
  -> Tasks: 115 successful, 115 total   (51 typecheck tasks across 76 packages)

Reverse verification, direction predicted before it ran. Prediction: with the nine annotations back at Promise[any] and the pins present, driver-sql's typecheck fails with exactly 10 TS2322 (5 doors x IsAny + Equals) and driver-turso's with exactly 8 (4 doors x 2), both confined to the pin files. Measured, exactly as predicted:

driver-sql   tsc --noEmit -> 10 x TS2322, all in src/sql-driver-doors-declared-types.test.ts   (lines 91-100)
driver-turso tsc --noEmit ->  8 x TS2322, all in src/turso-driver-doors-declared-types.test.ts (lines 86-93)

plus one TS7006 per package, in the same pin files, where a runtime case's map callback loses its parameter type once bulkCreate is any again. The mutation was proved on disk before the run was read (5 and 4 annotations at Promise[any], 0 narrowed) and the restore leg is proved by blob hash, not by an exit code: git hash-object on both files equals git rev-parse HEAD:[path], and git diff HEAD is empty. The pins import their subject relatively (./index.js, ./turso-driver.js), so they resolve to source rather than through a dependency's exports — the ablation-dist-preflight hazard's stated condition does not apply, and the RED result is itself the proof of source resolution.

Tests — no new failures, nothing skipped:

driver-sql          164 files passed, 11 skipped (pre-existing) | 2456 tests passed
driver-turso         51 files passed | 1236 tests passed
driver-sqlite-wasm   29 files passed |  518 tests passed
runtime (the changed probe)  1 file passed | 6 tests passed

Gates — the family derived from the diff itself, node scripts/pm/dispatch-gates.mjs, change set taken off the merge base rather than a hand-written list:

61 derived families, 61 run, 0 NOT-MEASURED, 0 UNRUN   (--ran reconciliation)
59 exit 0 on the first pass.
check:type-check-debt        first pass exit 3 = PREREQUISITE NOT MET (dist older than sources
                             after the ablation restored their mtimes) -- not a red, nothing
                             measured; after a forced rebuild: exit 0, 5 ledger entries
                             re-measured, none above its recorded number.
check:dual-build-cjs-loads   first pass exit 3 = PREREQUISITE NOT MET (unbuilt packages); after
                             building ./packages/* and ./packages/*/*: exit 0, 104 published
                             require entry points across 67 packages load.
check:adr-0087-registration  exit 0 -- 2 declared-breaking changesets, each carrying a
                             disposition, each narrowing verified symbol by symbol.

Every exit code was captured by redirect before any pipe, never across one.

Lint — the repo-wide run, not a narrowed one: pnpm lint (eslint . --no-inline-config) exits 0 over the whole population, measured on 07a0244.

Deliberately not done

  • analyzeQuery() — not an IDataDriver member; left Promise[any] by ruling.
  • aggregate() — left Promise[any] by ruling. Worth recording that the ruling's stated premise is wrong: the card's census says aggregate() is "not on the contract", but data-driver.ts does declare aggregate?(object, query, options): Promise[Record[string, unknown][]]. The instruction was to leave it, so it is untouched; the premise is reported rather than acted on.
  • TursoDriver.upsert()Promise[Record[string, any]], noted-not-counted on the card; untouched.
  • TursoDriver.beginTransaction()Promise[any]; never in scope.
  • packages/spec/ — untouched, per ruling 1.
  • No pin was added to driver-sqlite-wasm (PR feat(driver-sql,driver-turso): update() publishes its honest type — the contract's Record[string, unknown] | null, not any (#14438) #15280 added one there for update()). Ruling 1 fenced that package out of the diff, and ruling 4's precedent-wins clause is scoped to the grade; its source-level repairs are limited to the callers the narrowing broke.

Generated by Claude Code

…h their declared types, not any (#15267)

`SqlDriver` published an explicit `Promise<any>` over five doors the contract
had already declared narrower — `findOne`, `create`, `bulkCreate`, `execute`
and `explain` — so the emitted `.d.ts` erased every one of them. `TursoDriver`
overrides four of the same five with its own `Promise<any>`, which no
driver-sql fix reaches.

Each annotation is replaced with the type
`packages/spec/src/contracts/data-driver.ts` already declares for that door,
and each is pinned at the type level inside its own package's tsc program.
No runtime behaviour changes.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
…doors surfaced (#15267)

The narrowing surfaced 68 un-narrowed dereferences, every one in the two
packages' own tests: 64 reads of a `findOne()` result with no `null` check
(`expect(...).not.toBeNull()` does not narrow), and four reads through the
`unknown` that `bulkCreate()`, `explain()` and `findOne()` now resolve to.

Each positive control asserts the row arm with vitest's `assert()` — a
narrowing assertion, not a `!` and not a cast — and each `unknown` read names
what it reads. The not-found controls keep their `toBeNull()` and gain nothing.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
…5267)

`SqliteWasmDriver` overrides none of the five doors and inherits every one, so
the driver-sql narrowing reaches its callers through that package's `.d.ts`.
Eight positive controls assert the row arm; one `create()` read names the
string it collects. No source change in this package.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
…rity probe (#15267)

`SqlDriver.create()` declares the contract's `Record<string, unknown>` now, so
the cross-side parity probe's `rec_no` read is `unknown` where it was reached
through an `any`. It converts to the string the probe compares.

Caught by this package's `check:test-typecheck` gate, not by `tsc --noEmit` —
the file is in the checked test zone and the ledger does not cover it.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
#15267)

Both graded `minor` and marked `type-surface-only` under ADR-0087, matching
the landed precedent PR #15280 for `update()` on the same classes.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
…-0087 markers (#15267)

`isErasedType` counts `unknown` as erased by design (pinned TSO-U6), so the
`execute` / `explain` doors — which move onto the contract's own `unknown` —
cannot serve as predicate-4 evidence. The markers name the three doors that
move onto concrete shapes and state the rest in prose; the disposition is
identical for every door.

Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/driver-turso, touching 4 documentable anchor(s).

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5a95b0e93d048d96736a5ab678642416b6277c42.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5a95b0e93d048d96736a5ab678642416b6277c42packageMentionDocs.

Which tree this was computed on

This run read content/docs from a38cec6aeccee4ff813914f732d5a6b50fe24d7e — the merge of head 07a0244496587000f6ddeccd2d9ddd356b2e7aaa into base 5a95b0e93d048d96736a5ab678642416b6277c42, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a38cec6aeccee4ff813914f732d5a6b50fe24d7e && git checkout a38cec6aeccee4ff813914f732d5a6b50fe24d7e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a95b0e93d048d96736a5ab678642416b6277c42 07a0244496587000f6ddeccd2d9ddd356b2e7aaa && git checkout -B drift-repro 5a95b0e93d048d96736a5ab678642416b6277c42 && git merge --no-ff 07a0244496587000f6ddeccd2d9ddd356b2e7aaa

node scripts/docs-audit/affected-docs.mjs --json 5a95b0e93d048d96736a5ab678642416b6277c42

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5a95b0e93d048d96736a5ab678642416b6277c42 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants